<?php

/**! dzPurkki is a container for data read from database. dzPurkki has
 * both the changed and the orginal data.
 *
 * It is necessary to understand how start/finishEdit and cancel/commitNew
 * methods work and how they are used in order to get the best out of
 * controlled row inserting and inserting.
 *
 * But before anything, it is most important to declare groups. A group
 * contains column names so that when deleting or inserting a row, a group id
 * is specified in order to remove the same row in each column in same group.
 * All clear this far? If not, repeat until done or check the source code ;)
 *
 * startEdit should be called before any editing takes place and should be
 * finnished with finishEdit after all editing is done. startEdit adds an
 * extra row to the end of every column in every group. finishEdit removes
 * them.
 *
 * Between startEdit and finishEdit only commitNew and/or calcelNew is called.
 * These two takes care that there is always one new row present in specified
 * group. Now, every time this new row is edited, it should then either
 * commited or cancelled.
 * Inserting of a new row to certain location instead of to the end can be done
 * though these two methods, too, by passing the row no. as the second
 * parameter, which defaults to -1 (as last row).
 * All clear this far? If not, repeat until done or look at the source code ;)
 *
 * Deleting a row is only possible inside startEdit and finishEdit.
 * delGroupRow method does that what expected, but only if commitRow or
 * cancelNew has been called with new row position at -1 (to the end). In
 * practice deleting a rows works like this:
 * cancelNew($GroupID, -1);
 * delGroupRow($GroupID, $RowNo);
 *
 * Inserting a row with insGroupRow to a group works very much like deleting
 * one. Adding a row to the end of the group is possible by passing -1 as the
 * row position to add a row at. Example of using insert:
 * cancelNew($GroupID, -1);
 * insGroupRow($GroupID, $RowNo);
 *
 * $Id: dzpurkki.php 597 2006-04-03 16:15:46Z jyry $
 */

include_once("sobaseclass.php");
include_once("sodebug.php");
include_once("sostring.php");
include_once("soarrayset.php");
include_once("soconst.php");

/** The following is the list of stores dzPurkki contains.
    The list is comma separated list of store names. Spaces
    are not allowed. */
define(DZ_STORE_LIST,
       "TAFFEL,TISKI,TULLI,CLIP,LAST_QUERY");  /** list of valid stores */
define(TISKI, "TISKI");                 /** modified store */
define(TAFFEL, "TAFFEL");               /** original store */
define(TULLI, "TULLI");                 /** filtered data store */
define(CLIP, "CLIP");                   /** Data from the sub view.
                                            ()kind of clipboeard) */
define(LAST_QUERY, "LAST_QUERY");       /** The data of the last query */


/** dzPurkki is considered with data storing.
 * The data read from data base will be stored in this class. The class holds
 * both the orginal and the changed data. Data filtering is also implemented
 * here.
 */
class dzPurkki extends soBaseClass {

    var $__Stores;         /** array/soSet Contains the data stores.
                            DZ_STORE_LIST definition above defines different
                            data stores. Currently TAFFEL and TISKI are the
                            only stores. These are referenced with keywords:
                            TAFFEL and TISKI. Taffel contains the orginal data
                            and tiski the the data the user has changed. */
    var $__Groups;         /** soSet/dzGroup Template->__Cache should contain
                            *  this information. It is advisable to assign this
                            *  by calling setupGroups as soon as possible after
                            *  creating the object.
                            */
    var $__Editing = false;/** bool Flag indicating whether TISKI is under
                            * heavy editing or not.
                            */

    /** Initialises object.
     */
    function dzPurkki() {
        $this->initStores();
        $this->__Groups = new soSet();
    } // constructor


    /** Clears all stores
     */
    function initStores() {
        $store_list = soExplode(DZ_STORE_LIST, ",");
        foreach ($store_list as $store_id) {
            $this->__Stores[$store_id] = new soArraySet();
        } // foreach
    } // initStores


    /** Init one store. All column information is lost.
     *  @param string $StoreID Store to check
     */
    function initStore($StoreID) {
        if ($this->isStore($StoreID)) {
            $this->__Stores[$StoreID] = new soArraySet();
        };
    } // initStore


    /** Returns true is StoreID is valid store and all ColNames exists in it.
     *  @param string $StoreID Store to check
     *  @param array/string $ColNames COlumn names to check,
     *  @return bool Returns true(1) if all column names exists in the store,
     *      otherwise returns false(0).
     */
    function validColNames($StoreID, &$ColNames) {
        if (!$this->isStore($StoreID)) return 0; // <-- EXIT!!

        foreach ($ColNames as $col_name) {
            if (!$this->__Stores[$StoreID]->itemExists($col_name)) {
                 return 0; // <-- EXIT!!
            };
        }; // foreach

        return 1;
    } // validColNames


    /** Check that store with given name exists
     *  @param string $StoreID Store name to check
     *  @return bool True if name is ok. Otherwise false
     */
    function isStore($StoreID) {
        $result = is_object($this->__Stores[$StoreID]);
        $this->Error = !$result;
        return $result;
    } // isStore


    /** Gets column values from a store.
     *  @param string $StoreID Store to get col values from.
     *  @param string $ColName Column name.
     *  @return array/any Column values.
     */
    function getStoreCol($StoreID, $ColName) {
        $ColName = $this->__removeDatatype($ColName);
        if (!$this->isStore($StoreID)) {
            return NULL;
        } else {
            return $this->__Stores[$StoreID]->getItem($ColName);
        };
    } // getStoreCol


    /** Sets column values to a store.
     *  @param string $StoreID The store to store col values to.
     *  @param string $ColName Column name.
     *  @param array/any $Values Column values.
     *  @param bool $Replace When set(1), replaces any existing column by the 
     *      same name, otherwise simply skips the assignment.
     *  @return bool Returns true(1) on success or false(0) on failure
     */
    function setStoreCol($StoreID, $ColName, $Values, $Replace = 1) {
        $ColName = $this->__removeDatatype($ColName);
        if (!$this->isStore($StoreID)) {
            return 0;
        } else {
            return $this->__Stores[$StoreID]->setItem($ColName, 
                                                      $Values,
                                                      $Replace);
        };
    } // setStoreCol


    /** Adds array values to existing store column.
     *  Adds an array to the end of existing store column.
     *  @param string $StoreID The store to store col values to.
     *  @param string $ColName Column name.
     *  @param array/ny $Values Column values to add.
     *  @return bool Returns true(1) on success or false(0) on failure
     */
    function addToStoreCol($StoreID, $ColName, $Values) {
        $ColName = $this->__removeDatatype($ColName);
        if ((!$this->isStore($StoreID)) ||
            (!is_array($Values))) return 0; // <-- EXIT

        $set = $this->__Stores[$StoreID]->getItem($ColName);
        if ($set == NULL) $set = array();
        $set = array_merge($set, $Values);
        $this->__Stores[$StoreID]->setItem($ColName, $set);

        return 1;
    } // addToStoreCol


    /** Deletes a column from a store.
     *  @param string $StoreID the store to delete a column from.
     *  @param string $ColName Column name.
     *  @return bool Returns true(1) if specified store and column existed or 
     *      false(0) if not.
     */
    function delStoreCol($StoreID, $ColName) {
        if (!$this->isStore($StoreID)) return NULL; // <-- EXIT!!
        $ColName = $this->__removeDatatype($ColName);

        return $this->__Store[$StoreID]->delItem($ColName);
    } // delStoreCol


    /** Returns a value from specified store from specified column and row.
     *  @param string $StoreID The name of the store
     *  @param string $ColName The name of the column in the store
     *  @param int $RowNo The index of the row to get.
     *  @return any The asked value or NULL in case of error
     */
    function getStoreItem($StoreID, $ColName, $RowNo) {
        if (!$this->isStore($StoreID)) return NULL; // <-- EXIT!!
        $ColName = $this->__removeDatatype($ColName);

        return $this->__Stores[$StoreID]->getArrayItem($ColName, $RowNo);
    } // getStoreItem


    /** Stores a value for specified data store and column and row.
     *  @param string $StoreID The name of the store
     *  @param string $ColName The name of the column in the store
     *  @param int $RowNo The index of the row to get.
     *  @param string $Value The value to store
     *  @return bool true if all ok. Otherwise false.
     */
    function setStoreItem($StoreID, $ColName, $RowNo, $Value) {
        if (!$this->isStore($StoreID)) return 0; // <-- EXIT
        $ColName = $this->__removeDatatype($ColName);

        return $this->__Stores[$StoreID]->setArrayItem($ColName,
                                                       $RowNo,
                                                       $Value);
    } // setStoreItem


    /** Gets the values of the all columns from the specified store
     *  @param string $StoreID Store which columns to get
     *  @return soArraySet/any Returns Store values or NULL if StoreId is 
     *  not valid.
     */
    function getStore($StoreID) {
        if (!$this->isStore($StoreID)) return NULL; // <-- EXIT!!
        return $this->__Stores[$StoreID];
    } // getStore


    /** Sets values for all columns at once in one store
     *  @param string $StoreID Store which columns to set
     *  @param soArraySet/any $Cols Values
     *  @return bool Returns true(1) if StoreId was valid, otherwise false(0).
     */
    function setStore($StoreID, $Cols) {
        if (!$this->isStore($StoreID)) return 0; // <-- EXIT!!
        $this->__Stores[$StoreID] = $Cols;
        return 1;
    } // setStore


    /** Removes datatype ":type" from colnames.
       *  If any column names has trailing :datatypestr it then removed. This
       *      function is used by several methods in this class. It is to reduce
       *      debugging time as soDa object uses heavily table.col:type notation.
       *  @param string $ColNames Space delimited list or an array of col names:
       *  "table.name:type next.col"
       *  @param bool $ReturnArray When set, returns colnames as array instead 
       *  of space delimited string list.
       *  @return str Returns the column names string with all datatype declarations
       *    removed.
       */
    function __removeDatatype($ColNames, $ReturnArray = 0) {
        if (!is_array($ColNames)) $ColNames = soExplode($ColNames, " ");
        $i = count($ColNames);
        while (--$i >= 0) {
            $ColNames[$i] = soExtractVal($ColNames[$i], ":", 1);
        }; // foreach

        if ($ReturnArray) {
            return $ColNames;
        } else {
            return implode(" ", $ColNames);
        };
    } // __removeDatatype


    /** Sets up groups for inserting and deleting rows etc.
     *  Note, no error checking whether colnames are valid or not.
     *  @param object soSet/dzGroup $Groups Group objects.
     *  @param array/string $Stores Store to setup. If left empty all stores
     *  are set up
     */
    function setupGroups(&$Groups, $Stores = NULL) {
        $this->__Groups = $Groups;

        $group_ids = $Groups->getKeys();
        foreach ($group_ids as $group_id) {
            $group = &$Groups->getItem($group_id);
            if (soisNull($Stores)) {
                $Stores = soExplode(DZ_STORE_LIST, ",");
            } // if $Store
            foreach ($Stores as $store) {
                foreach ($group->Cols as $col_name) {
                    $this->__Stores[$store]->setItem($col_name, array());
                }; // foreach $col
            }; // foreach $store
        }; // foreach $group_id

    } // setupGroups


    /** Returns a column list.
     *  Returns a space delimited list of columns names in specified group
     *  or as is when no such group exists.
     *  @param string $GroupID Group ID
     *  @return str Space delimited string of columns names.
     */
    function getGroupCols($GroupID) {
        /** check for group definition */
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) {
            /* no such group -- return the id as is */
            $result = $GroupID;
        } else {
            if (count($group->Cols)) $result = implode(" ", $group->Cols);
        };

        return $result;
    } // getGroupCols


    /** Get row count of a group.
     *  Returns a number of rows in specified store and group.
     *  @param string $StoreID Store ID
     *  @param any $GroupID Group id to get the row count of.
     *  @param int $Type 0=quick count, row count from 1st column only,
     *      1=tallest and -1=shortest of them all.
     *  @return int Returns the number of rows or -1 on error.
     */
    function getRowCount($StoreID, $GroupID, $Type = 0) {
        if (!$this->isStore($StoreID)) return -1; // <-- EXIT

        $col_names = soExplode($this->getGroupCols($GroupID), " ");
        return $this->__Stores[$StoreID]->getRowCount($col_names, $Type);
    } // getRowCount


    /** Returns a row count for specified column.
     *  >$StoreID:TISKI/TAFFEL Store ID
     *  >$ColName Column name to get row count for
     *  @return int Returns the number of rows or -1 on error.
     */
    function getColRowCount($StoreID, $ColName) {
        if (!$this->isStore($StoreID)) return -1; // <-- EXIT
        return $this->__Stores[$StoreID]->getRowCount(array($ColName), 0);
    } // getColRowCount


    /** Renumbers a TISKI column in a group.
     *  If a group has a column that is to be renumbered everytime a row is
     *  deleted or inserted to the group, then this function takes care of it.
     *  This only works with TISKI, naturally.
     *  @param int $GroupID GroupID
     *  @return bool Returns false(0) on error (bad group, no autonum col, 
     *  etc.), otherwise true(1).
     */
    function __renumber($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) return 0; // <-- EXIT
        if ($group->AutoNum == NULL) return 0; // <-- EXIT

        $pos = soArrayFind($group->Cols, $group->AutoNum);
        if ($pos == -1) return 0; // <-- EXIT

        $icount = $this->getRowCount(TISKI, $GroupID, 0);
        $col = array();
        for ($i = 0; $i < $icount; $i++) {
            $col[] = $i +1;
        }; // for $i

        $this->__Stores[TISKI]->setItem($group->Cols[$pos], $col);
        return 1;
    } // __renumber


    /** Inserts a row at a specified position, in a specified group in TISKI.
     *  @param int $RowNo The position where to insert the row. -1 represents
     *  "as last"
     *  @param string $ColNames GroupID in '#GroupID' format or space
     *  delimited list of columns to add a row to.
     *  @param string $StoreID Store ID
     *  @return int Returns row position of newly inserted row or -1 on error.
     */
    function insRow($RowNo = -1, $ColNames = "", $StoreID = TISKI) {
        if (!$this->isStore($StoreID)) return -1; // <-- EXIT
        if (!$ColNames) $ColNames = implode(" ", $this->getColNames($StoreID));

        $result = $this->resizeCols(1, $StoreID, $RowNo, $ColNames);

        if ($result == false) {
            $result = -1;
        } else {
            $col_arr = soExplode($ColNames, " ");
            if ($RowNo == -1) {
                $store = &$this->__Stores[$StoreID];
                $result = count($store->getItem($col_arr[0])) -1;
            } else {
                $result = $RowNo;
            };
        };

        return $result;
    } // insRow


    /** Inserts a row at a specified position and group in TISKI.
     *  @param int $RowNo The position where to insert the row. -1 represents
     *  "as last"
     *  @param $GroupID GroupID to which to add a row to.
     *  @return int Returns row position of newly inserted row or -1 on error.
     */
    function insGroupRow($GroupID, $RowNo) {
        $group = $this->__Groups->getItem($GroupID);

        if ($this->__Groups->Error) return -1; // <-- EXIT
        if (($group->Inserting == DZGI_NONE) ||
            (($group->InsCount) && (in_array($group->Inserting,
                                             array(DZGI_ONE_ANY,
                                                   DZGI_ONE_END)
                                                   )))) {
            return -1; // <-- EXIT
        };
        if (($group->RowLimit) &&
            ($this->getRowCount(TISKI, $GroupID) >= $group->RowLimit)) {
            return -1; // <-- EXIT
        };

        $result = $this->insRow($RowNo, implode(" ", $group->Cols));
        if ($result != -1) {
            $this->__renumber($GroupID);
            $group = $this->__Groups->getItem($GroupID);
            $this->__Groups->setItem($GroupID, $group);
        } // if

        return $result;
    } // insGroupRow


    /** Deletes a row at specified position and group in TISKI.
     *  Note! This only works if startEdit has been started and
     *  cancel/commitRow has been called with new row pos at -1.
     *  @param $RowNo:int The index of the row to delete.
     *  @param $ColNames:str GroupID in '#GroupID' format or space delimited
     *  list of columns to delete a row from.
     *  @return bool true if all ok. Otherwise false.
     */
    function __delGroupRow($GroupID, $RowNo) {
        soDebug("dzPurkki: __delGroupRow($GroupID, $RowNo)", SOD_DET);
        $group = $this->__Groups->getItem($GroupID);

        /* Exit right away when
         * - bad group id
         * - row no. to delete is out of range */
        if ($this->__Groups->Error) return false; // <-- EXIT
        if ($RowNo >= count($this->getStoreCol(TISKI, $group->Cols[0]))) {
            return false; // <-- EXIT
        };

        $result = $this->delRow($RowNo, implode(" ", $group->Cols));
        if ($result != -1) {
            $this->__renumber($GroupID);
            $group = $this->__Groups->getItem($GroupID);
            $this->__Groups->setItem($GroupID, $group);
        } // if

        return $result;
    } // __delGroupRow


    /** Deletes a row at specified position and columns in TISKI.
     *  @param int $RowNo The index of the row to delete.
     *  @param string $ColNames An array or space delimited list of columns
     *  to delete a row at.
     *  @return bool true if all ok. Otherwise false.
     */
    function delRow($RowNo, $ColNames = "") {
        $ColNames = $this->__removeDatatype($ColNames, 1);
        if (!count($ColNames)) {
            $ColNames = $this->getGroupCols($ColNames);
        }; // if
        $result = $this->resizeCols(-1, TISKI, $RowNo, $ColNames);

        return $result;
    } // delRow


    /** Resizes columns in rows.
     *  @param int $Rows Number of rows to delete (negative values) or insert
     *      (positive values) for specified columns.
     *  @param string $StoreID The name of the store to insert the value
     *  @param string $ColNames An array or space delimited list of columns to
     *  add a row to.
     *  @param int $RowPos Row no. at which to focus the delete or insert
     *  operation. -1 stands for "insert as last element".
     *  @return bool true if all ok. Otherwise false.
     */
    function resizeCols($Rows, $StoreID, $RowPos, $ColNames = "") {
        $ColNames = $this->__removeDatatype($ColNames, 1);
        if ((($RowPos < -1)) || (!$this->isStore($StoreID))) {
            return false; // <-- EXIT!!
        }; // if

        if (!count($ColNames)) {
            $ColNames = $this->getColNames($StoreID);
        }; // if

        $icount = abs($Rows);
        foreach ($ColNames as $key) {
            if ($this->__Stores[$StoreID]->itemExists($key)) {
                $col_arr = $this->__Stores[$StoreID]->getItem($key);
                for ($i = 0; $i < $icount; $i++) {
                    if ($Rows < 0) {
                        $result = soDelElement($col_arr, $RowPos);
                    } else {
                        $result = soInsElement($col_arr, "", $RowPos);
                    }; // else if
                }; // for $i

                if ($result) {
                    $this->__Stores[$StoreID]->setItem($key, $col_arr);
                };
            }; // if isCol
        }; // foreach

        return true;
    } // resizeCols


    /** Get the column values of the specified row and store.
     *  @param string $StoreID Store name where to get the row.
     *  @param int $Row Row number
     *  @param array||string $ColNames Space delimited list or array of column
     *  names. A NULL  means "return values for all columns".
     *  @param bool $AsArray When set, returns the row as an array, instead
     *  of soSet.
     *  @return soSet/any Returns column values as a soSet or an empty set on 
     *  error.
     */
    function getRow($StoreID, $RowNo, $ColNames = NULL, $AsArray = 0) {
        $result = new soSet();
        if (!$this->isStore($StoreID)) return $result; // <-- EXIT!!

        $ColNames = $this->__removeDatatype($ColNames, 1);
        if (!count($ColNames)) $ColNames = $this->getColNames($StoreID);

        foreach ($ColNames as $key) {
            $values = $this->__Stores[$StoreID]->getItem($key);
            $result->setItem($key, $values[$RowNo]);
        } // foreach

        if ($AsArray) {
            return $result->getAsArray();
        } else {
            return $result;
        }; // else if
    } // getRow
    
    
    /** Moves a row to another position.
     *  @param string $StoreID Store name where to get the row.
     *  @param int $FromRowNo Row no. of a row to move.
     *  @param int $ToRowNo Target position to move the row to.
     *  @param array||string $ColNames Space delimited list or array of column
     *  names to move. A NULL value applies to all items.
     *  @return bool True if store name and both rownos. are valid, 
     *  otherwise false.
     */
    function moveRow($StoreID, $FromRowNo, $ToRowNo, $ColNames) {
        if (!$this->isStore($StoreID)) return 0; // <-- EXIT!!
        $row = $this->getRow($StoreID, $FromRowNo);
        if (!$row->size()) return 0; // <-- EXIT
        $this->delRow($StoreID, $FromRowNo, $ColNames);
        return $this->setRow($StoreID, $ToRowNo, $row);
    } // moveRow


    /** Finds a row in store with specified values.
     *  Returns a row no. of a row where row values searched matches.
     *  $ColNames may be used to limit the search to certain columns only.
     *  Column count must match with column values. If no column names are
     *  specified, all columns are used.
     *  @param TISKI/TAFFEL $StoreID Store ID
     *  @param array/any Search values
     *  @param str Space delimited list of column names or "" for all cols.
     *  @return int Row no., or -1 when now such row found (or bad store id)
     */
    function findRow($StoreID, $ColVals, $ColNames = "") {
        if (!$this->isStore($StoreID)) return -1; // <-- EXIT!!

        $ColNames = $this->__removeDatatype($ColNames, 1);
        if (!count($ColNames)) $ColNames = $this->getColNames($StoreID);

        $vals = serialize($ColVals);
        $r = $this->getRowCount($StoreID, $ColNames);
        $result = -1;
        while ((!$result) && (--$r >= 0)) {
            $row = $this->getRow($StoreID, $r, $ColNames);
            if (serialize($row->getAsArray(1)) == $vals) $result = $r;
        }; // while

        return $result;
    } // findRow


    /** Stores values as a row across one or more columns
     *  @param string $StoreID Store name where to get the row.
     *  @param int $Row Row number
     *  @param soSet/any $Values Values to store.
     *  @return bool Returns true(1) on success, else false(0).
     */
    function setRow($StoreID, $RowNo, $Values) {
        if (!$this->isStore($StoreID)) return 0; // <-- EXIT!!

        $col_names = $this->__removeDatatype($Values->getKeys(), 1);
        foreach ($col_names as $col_name) {
            if ($this->setStoreItem($StoreID, $col_name, $RowNo,
                                    $Values->getItem($col_name))) {
                return 0; // <-- EXIT
            };
# WHY THIS?            $this->getStoreItem($StoreID, $col_name, $RowNo);
        } // foreach

        return 1;
    } // setRow


    /** Get columns names as an array from one store.
     *  @param string $StoreID The name of the store
     *  @return array/str Returns an array of column names. An empty array is
     *       returned if the StoreId is invalid or there are no cols in the
     *       store.
     */
    function getColNames($StoreID) {
        if (!$this->isStore($StoreID)) return array(); // <-- EXIT!!
        return $this->__Stores[$StoreID]->getKeys();
    } // getColNames


    /** Empties the data store.
     *  Simply clears out all data in the specified store.
     *  @param string $StoreID The name of the store to clear
     *  @return bool Returns true(1) StoreId is valid, otherwise returns
     *  false(0).
     */
    function emptyStore($StoreID) {
        if (!$this->isStore($StoreID)) return 1; // <-- EXIT!!
        $store = $this->__Stores[$StoreID];
        $keys = $store->getKeys();

        foreach ($keys as $key) {
            $store->setItem($key, array());
        } // foreach

        $this->__Stores[$StoreID] = $store;

        return 1;
    } // emptyStore


    /** Starts editing of TISKI
     *  @return bool Returns true(1) when edit has not yet been started or
     *      false(1) if it has.
     */
    function startEdit() {
        soDebug("dzPurkki->startEdit()", SOD_DET);
        if ($this->__Editing) return 0; // <-- EXIT
        $this->__Editing = 1;

        $group_ids = $this->__Groups->getKeys();
        foreach ($group_ids as $group_id) {
            $group = $this->__Groups->getItem($group_id);
            $group->DelCount = 0;
            $group->InsCount = 0;
            $this->__Groups->setItem($group_id, $group);
            $this->__startNew($group_id, -1);
        }; // foreach $group

        return 1;
    } // startEdit


    /** Finishes editing of TISKI
     *  @return bool Returns false(0) when edit has not yet been started or
     *      true(1) if it has.
     */
    function finishEdit() {
        soDebug("dzPurkki->finishEdit()", SOD_DET);
        if (!$this->__Editing) return 0; // <-- EXIT
        $this->__Editing = 0;

        $group_ids = $this->__Groups->getKeys();
        foreach ($group_ids as $group_id) {
            $this->cancelNew($group_id, -2);
        }; // foreach $group

        return 1;
    } // finishEdit


    /** Start new row edit.
     *  @param any $GroupID Group ID to add a new row to.
     *  @param int $RowNo Row position to add a row at, -1 == to the end
     *  @return bool Returns true(1) on success, or false(0) on error such as
     *      bad GroupID or row edit already started.
     */
    function __startNew($GroupID, $RowNo = -1) {
        soDebug("dzPurkki: __startNew($GroupID, $RowNo)", SOD_DET);
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) return 0; // <-- EXIT
        if ($group->NewRowAt != -1) return 0; // <-- EXIT

        /* If this is main group just make sure that there is one row. */
        if ($this->getGroupType($GroupID) == DZGT_MAIN) {
            if ($this->getRowCount(TISKI, $GroupID) < 1)
                $this->insGroupRow($GroupID, -1);
        } else {
            $new_row = $this->insGroupRow($GroupID, $RowNo);
            /* Reread group information.insGroupRow above changes it. */
            $group = $this->__Groups->getItem($GroupID);
            $group->NewRowAt = $new_row;
            $this->__Groups->setItem($GroupID, $group, 1);
        } // if

        return 1;
    } // __startNew


    /** Cancel new row edit.
     *  @param any $GroupID Group ID to cancel new row edit at.
     *  @param  int $NextNewRowNo Row no. to start a new row edit at. Value -2
     *      is reserved by the system and is used by finishEdit so that
     *      no new row gets added. -1 == add next row to the end.
     *  @return bool Returns true(1) on success, or false(0) on error such as
     *      bad GroupID or no row edit started yet.
     */
    function cancelNew($GroupID, $NextNewRowNo = -1) {
        soDebug("dzPurkki: cancelNew($GroupID, $NextNewRowNo)", SOD_DET);
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) return true; // <-- EXIT
        if ($group->NewRowAt == -1) return true; // <-- EXIT
        if ($this->getGroupType($GroupID) == DZGT_MAIN)
            return false; // <-- EXIT


        $this->__delGroupRow($GroupID, $group->NewRowAt);
        /* Reread group information.__delGroupRow above changes it. */
        $group = $this->__Groups->getItem($GroupID);

        /* Mark new row removed */
        $group->NewRowAt = -1;
        $this->__Groups->setItem($GroupID, $group, 1);
        if ($NextNewRowNo > -2) $this->__startNew($GroupID, $NextNewRowNo);

        return 0;
    } // cancelNew


    /** Commit new row edit.
     *  @param any $GroupID Group ID to commit new row edit at.
     *  @param int $NextNewRowNo Row no. to start a new row edit at. -1 == to 
     *  the end
     *  @return bool Returns true(1) on success, or false(0) on error such as
     *      bad GroupID or no row edit started yet.
     */
    function commitNew($GroupID, $NextNewRowNo = -1) {
        soDebug("dzPurkki: commitNew($GroupID, $NextNewRowNo)", SOD_DET);
        $group = $this->__Groups->getItem($GroupID);
        if (($this->__Groups->Error) ||
            ($group->NewRowAt == -1) ||
            ($this->getGroupType($GroupID) == DZGT_MAIN)) return 0; // <-- EXIT

        /* Mark new row as normal row */
        $group->NewRowAt = -1;
        $group->InsCount++;
        $this->__Groups->setItem($GroupID, $group, 1);
        soDebug("dzPurkki: InsCount=".$group->InsCount, SOD_DET);
        /* Add new line into the group */
        $this->__startNew($GroupID, $NextNewRowNo);
        return 1;
    } // commitNew


    /** Delete row from a group.
     *  This method is considered with New line too
     *  @param any $GroupID Group ID to cancel new row edit at.
     *  @param int $Row Row number
     *  @return bool Returns true(1) on success, or false(0) on error.
     */
    function delGroupRow($GroupID, $RowNo) {
        soDebug("dzPurkki: delGroupRow($GroupID, $RowNo)", SOD_DET);
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) return false; // <-- EXIT
        if ($this->getGroupType($GroupID) == DZGT_MAIN)
            return false; // <-- EXIT
        /* Exit right away when
         * - no deleting allowed
         * - trying to delete more than one when only one is allowed per edit
         */
        if (($group->Deleting == DZGD_NONE) ||
            (($group->DelCount) && ($group->Deleting == DZGD_ONE_ANY))) {
            return false; // <-- EXIT
        };


        /* Check if we are deleting new line */
        if ($group->NewRowAt != $RowNo) {
            /* Remove the new line first */
            $this->cancelNew($GroupID, -2);
            $this->__delGroupRow($GroupID, $RowNo);

            /* Increase del count */
            $group = $this->__Groups->getItem($GroupID);
            $group->DelCount++;
            soDebug("dzPurkki: delCount=".$group->DelCount, SOD_DET);
            $this->__Groups->setItem($GroupID, $group, 1);

            $this->__startNew($GroupID);
        } else {
            /* Just remove the new line and insert a new one in the end
               of the group */
            $this->cancelNew($GroupID);
        } // if $group->NewRowAt

        return true;
    } // delGroupRow


    /** Get the group's deleting limit.
     *  @param any $GroupID Group ID which limit to get
     *  @return string Defined deleting limit or NULL if the group id was not
     *  found
     */
    function getDeleteLimit($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) $result =  NULL;
        else $result = $group->Deleting;

        return $result;
    } // getDeleteLimit


    /** Get the group's inserting limit.
     *  @param any $GroupID Group ID which limit to get
     *  @return string Defined inserting limit or NULL if the group id was not 
     *  found
     */
    function getInsertLimit($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) $result =  NULL;
        else $result = $group->Inserting;

        return $result;
    } // getInsertLimit


    /** Get the group's delete count
     *  @param any $GroupID Group ID which delete count to get
     *  @return string Current delete count or NULL if the group id was not 
     *  found
     */
    function getDelCount($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) $result =  NULL;
        else $result = $group->DelCount;

        return $result;
    } // getDelCount


    /** Get the group's insert count
     *  @param any $GroupID Group ID which insert count to get
     *  @return string Current insert count or NULL if the group id was not
     *  found
     */
    function getInsCount($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) $result =  NULL;
        else $result = $group->InsCount;

        return $result;
    } // getInsCount


    /** Get the new row position in a group
     *  @param any $GroupID Group ID to add a new row to.
     *  @return int The row index of new row in given group or null if group
     *      does not exist.
     */
    function getNewRowNo($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) $result =  NULL;
        else $result = $group->NewRowAt;

        return $result;
    } // getNewRowNo


    /** Get the type of the group
     *  @param any $GroupID Group ID to add a new row to.
     *  @return int Group type
     */
    function getGroupType($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        if ($this->__Groups->Error) {
            $result =  NULL;
        } else {
            $result = $group->Type;
        };

        return $result;
    } // getGroupType


    /** Copies a store to another
     *  @param string $FromStore The name of the store to copy from
     *  @param string $ToStore The name of the store to copy to
     *  @return bool Returns true(1) if StoreId is valid, otherwise returns
     *  false(0).
     */
    function copy($FromStore, $ToStore) {
        if (!$this->isStore($FromStore) ||
            (!$this->isStore($ToStore))) return 0; // <-- EXIT!!

        $copy = $this->__Stores[$FromStore];
        $this->__Stores[$ToStore] = unserialize(serialize($copy));

        return 1;
    } // copy


    /** Joins a soSet object into TISKI.
     *  ItuSet contains instructions and data what to do with store specified
     *  by SoreID. Set key can take a form of 'col_name', or 'colname#x'
     *  where x is instruction where to put and what to do. Instuction may be
     *  one of the following:
     *
     *  empty ''    Replaces the current row value. If more than one value
     *              if provided, only the first one is used.
     *  -           Deletes row value(s) specified in set value(s). NULL value
     *              ("") stands for "current row".
     *  +pos        Insert value(s) *before* row 'pos'. '++' Add value(s) after
     *              the last existing element, '+' insert value(s) *before*
     *              current, focus, row.
     *  Example: Key="m.foo#++",Value=array("bar") would set value "bar" as
     *      the last element in column "m.col"
     *  Example: Key="m.foo#+1",Value=array("b", "a", "r") would insert values
     *      "a", "b" and "c" before the second element in column "m.foo".
     *  Example: Key="m.foo#-",Value=array(1, 2, "") would delete elements 1,
     *      2 and the current row. Note! It it not advisable have both
     *      absolute row pos and current row pos references in the in the same
     *      array, as it may lead to strance behaviour as the same row might
     *      get deleted twice: array(3, 3) would actually delete 3 and 4.
     *  Example: Key="m.foo#",Value=array("bar") would set current row element
     *      in column "m.foo" to "bar".
     *  @param soSet $Set Set to join
     *  @return bool Returns true on success or false(0) on error.
     */
    function joinItu($Set) {
        $keys = $Set->getKeys();
        foreach ($keys as $key) {
            $new_rows = $Set->getItem($key);

            if (soStrPos($key, "#") == -1) {
                $this->setStoreCol(TISKI, $key, $new_rows);
            } else {

                /* v- [0]=colname, [1]=instruction */
                $split = soSplitStr($key, "#", 1);
                $icount = count($new_rows) -1;
                for ($i = $icount; $i >= 0; $i--) {
                    $this->__joinSet_sub($split[0], $split[1]);
                }; // for $i
            };
        }; // foreach

        return 1;
    } // joinItu


    /** Modifies TISKI column according to instruction.
     *  Called by joinItu
     *  @param string $Target Target column
     *  @param string $Instr Instructions
     */
    function __joinItu_sub($Target, $Instr) {
        $old_count = $this->getColRowCount(TISKI, $Target);
        $group_no = $this->findGroup($Target);
        if ($Instr == "-") {

            /* Delete */
            $this->resizeCols(-1, TISKI, $new_rows[$i], $Target);
        } else {
            if (!$Instr) {

                /* Set */
                $rowpos = $this->getFocusRow($group_no);
                $this->setStoreItem(TISKI, $Target,
                                    $rowpos,
                                    $new_rows[$i]);
            } else {

                if (substr($Instr, 0, 1) == "+") {
                    /* Insert */
                    switch ($split[1]) {
                        case "++": // as last
                            $rowpos = -1;
                            break;
                        case "+":  // before currect
                            $rowpos = $this->getFocusRow($group_no);
                            break;
                        default:   // before 'pos'
                            $rowpos = substr($split[1], 1);
                    }; // switch
                    $this->resizeCols(1, TISKI, $old_count, $Target);

                    if ($rowpos == -1) $rowpos = $old_count;

                    $this->setStoreItem(TISKI, $Target,
                                        $rowpos, $new_rows[$i]);
                } else {
                        // set - what is this for? --jq
                };
            };
            if ($rowpos != -1) {}
        }; // if
    } // joinItu_sub


    /** Returnd group no. for a column
     *  @param string $ColName Column name
     *  @return int Group no. where column belongs to or -1 if not found.
     */
    function findGroup($ColName) {
        $keys = $this->__Groups->getKeys();
        $found = 0;
        $k = count($keys);
        while ((!$found) && (--$k >= 0)) {
            $grp = $this->__Groups->getitem($keys[$k]);
            if (in_array($ColName, $grp->Cols)) $found = 1;
        };

        return $keys[$k];
    } // findGroup


    /** Returns filter group id or NULL
     *  If group has filter source column(s), returns dependent group ids.
     *  @param any $GroupID Group ID to check
     *  @return array/any Group ids or NULL that depends on GroupID
     */
    function getFilterGroupsByGroup($GroupID) {
        $group = $this->__Groups->getItem($group_id);
        if ($this->__Groups->Error) return NULL; // <-- EXIT

        if ($group->Filter->Source) {
            $this->Error = 0;
            return $group_id;
        } else {
            $this->Error = 1;
            return NULL;
        };
    } // getFilterGroupsByGroup


    /** Returns filtered group ids or NULL
     *  If column name belongs to a group that is filtered, returns .
     *  @param string $ColName Column name
     *  @return array/any Group ids or NULL if no filterd groups.
     */
    function getFilterGroupsByCol($ColName) {
        $group_id = $this->findGroup($ColName);
        $this->Error = ($group_id != -1);
        if ($group_id == -1) return NULL; // <-- EXIT

        return $this->getFilterGroupsByGroup($group_id);
    } // checkFilterGroup


    /** Returns currect, focus, row in TISKI.
     *  >$GroupID:any Group id.
     *  @return int Returns current row no, -1 = not set, -2 = group not found.
     */
    function getFocusRow($GroupID) {
        $group = $this->__Groups->getItem($GroupID);
        $this->Error = $this->__Groups->Error;
        if ($this->__Groups->Error) return -2; // <-- EXIT
        return $group->Filter->FocusRow;
    } // getFocusRow


    /** Sets currect, focus, row for TISKI.
     *  @param any $GroupID Group id.
     *  @param int $RowNo New focus row, -1 == unset
     *  @return bool Returns true(1) on success or false(1) on error such as 
     *      bad group id or row no out of range.
     */
    function setFocusRow($GroupID, $RowNo) {
        $group = $this->__Groups->getItem($GroupID);
        $this->Error = $this->__Groups->Error;

        if ($this->__Groups->Error) return 0; // <-- EXIT
        $rcount = $this->getRowCount(TISKI, $GroupID);

        if (($RowNo < -1) || ($RowNo >= $rcount)) {
            $this->Error = 1;
            return 0; // <-- EXIT
        };
        $this->Error = 0;
        $group->Filter->FocusRow = $RowNo;
    } // setFocusRow


    /** Returns distict rows from store in Purkki
     *  Returns rows for specified columns and store in purkki without
     *  possible duplicate values. Say, if TISKI contains 10 rows of which
     *  column A contains only values "1" and "2", and B contains "3"
     *  in every row, then a soSet of two columns (A and B) would be returned
     *  with A having values "1" and "2" and B 3" and "3".
     *  All in all, this works pretty much like distict in SQL.
     *  @param string $StoreID Store to get rows from.
     *  @param string $ColNames Space delimited list of columns to get rows 
     *      from. Blank value is considered as "all columns."
     *  @return soSet/array Requested columns and their distict rows or NULL on
     *      error.
     */
    function getDistinctRows($StoreID, $ColNames = "") {
        if (!$this->isStore($StoreID)) return NULL; // <-- EXIT
        if (!$ColNames) {
            $col_names = $this->__Stores[$StoreID]->getKeys();
        } else {
            $col_names = soExplode($ColNames, " ");
        }; // else if
        $ColNames = implode(" ", $col_names);


        /* Check that each column name is valid */
        if (!$this->validColNames($StoreID, $col_names)) {
            $this->Error = 1;
            return NULL; // <-- EXIT
        };

        /* Checks all done. Start getting rid of duplicates */
        $result = new soSet();
        $rcount = 0;
        $s_rows = $this->__Stores[$StoreID]->getItem($col_names[0]);
        $scount = count($s_rows);

        for ($s = 0; $s < $scount; $s++) {
            $found = 0;
            $r = 0;
            $s_row = $this->getRow($StoreID, $s, $ColNames);
            while ((!$found) && ($r < $rcount)) {
                $r_row = array();
                foreach ($col_names as $col_name) {
                    $r_vals = $result->getItem($col_name);
                    $r_row[] = $r_vals[$r];
                }; // foreach
                if (!count(soCompArrays($s_row->getAsArray(1), $r_row))) {
                    $found = 1;
                };

                $r++;
            }; // while

            if (!$found) {
                foreach ($col_names as $col_name) {
                    $r_vals = $result->getItem($col_name);
                    if ($r_vals == NULL) $r_vals = array();
                    $r_vals[] = $s_row->getItem($col_name);
                    $result->setItem($col_name, $r_vals);
                }; // foreach
                $rcount++;
            }; // if

        }; // for

        $this->Error = 0;
        return $result;
    } // getDistinctRows


    /** Updates row values in specified columns and row values.
     *  Compares given key values to Purkki values and when matched, replaces
     *  data col purkki values with gived data values. Say, if KeyCols (soSet)
     *  contains only column A with values "1" and "2", and DataCols (soSet)
     *  contains column B with values "x" and "y", then Purkki column A is
     *  scanned and any row containing "1" or "2" causes same row value in
     *  column B be replaced with value "x" or "y" accordingly.
     *  @param string $StoreID Store to update rows in.
     *  @param soSet/array $KeyCol Key columns and their values
     *  @param soSet/array $DataCol Data columns and their values
     *  @return int Returns number of rows updated or -1 on error such as bad
     *  StoreID
     */
    function updateRows($StoreID, $KeyCols, $DataCols) {
        $key_names = $KeyCols->getKeys();
        $data_names = $DataCols->getKeys();

        /* Check that each column name is valid */
        if ((!$this->validColNames($StoreID, $key_names)) ||
            (!$this->validColNames($StoreID, $data_names))) {
            $this->Error = 1;
            return -1; // <-- EXIT
        };

        $k_count = count($KeyCols->getItem($key_names[0]));
        if ($k_count != count($DataCols->getItem($data_names[0]))) {
            /* keycol row count does not match with data col row count */
            $this->Error = 1;
            return -1; // <-- EXIT
        }

        /* Purkki row count */
        $p_count = count($this->getStoreCol($StoreID, $key_names[0]));

        /* Loop and update */
        $result = 0;
        $n_vals = new soSet();
        for ($k = 0; $k < $k_count; $k++) {
            $vals = array();
            foreach ($key_names as $key_name) {
                $rows = $KeyCols->getItem($key_name);
                $vals[] = $rows[$k];
            }; // foreach

            for ($p = 0; $p < $p_count; $p++) {
                $p_vals = $this->getRow($StoreID, $p,
                                        implode(" ", $key_names));
                $diff = soCompArrays($vals, $p_vals->getAsArray(1));
                if (!count($diff)) {
                    $n_vals->clear();
                    foreach ($data_names as $data_name) {
                        $rows = $DataCols->getItem($data_name);
                        $n_vals->addItem($data_name, $rows[$k]);
                    }; // foreach
                    $this->setRow($StoreID, $p, $n_vals);
                    $result++;
                };
            }; // for
        }; // for

        $this->Error = 0;
        return $result;
    } // updateRows


    /** Returns group ids that are filtered.
     *  Returns group ids that are dependend on specified seed col or all,
     *  when left empty.
     *  @param  string $SeedCol When not "", limits the returned group ids to
     *      those that are dependend on SeedCol.
     *  @return array/any Group ids.
     */
    function getFilteredGroups($SeedCol = "") {
        $result = array();
        $keys = $this->__Groups->getKeys();
        foreach ($keys as $key) {
            $grp = $this->__Groups->getItem($key);
            if ((count($grp->Filter->Seed)) && (!$SeedCol ||
                (in_array($SeedCol, $grp->Filter->Seed)))) {
                $result[] = $key;
            }; // if
        }; // foreach

        $this->Error = 0;
        return $result;
    } // getFilteredGroups


    /** Returns seed group id for a group.
     *  Returns a group id that contains the seed col for specified group.
     *  @param any $GroupID Group id to get it's seed col group id for
     *  @return any Seed col group id or -1 when no seed group or error occured
     */
    function getFilterSeedGroup($GroupID) {
        $target_grp = $this->__Groups->getItem($GroupID);
        $this->Error = $this->__Groups->Error;
        if ($this->__Groups->Error) return -1; // <-- EXIT

        /* Look for seed group */
        $error = 0;
        $keys = $this->__Groups->getKeys();
        $result = count($keys);
        while ((--$result >= 0) && (!$error)) {
            $grp = $this->__Groups->getItem($keys[$result]);
            /* Are all seed cols found in this group? */
            $i = count($grp->Filter->Seed) -1;
            do {
                if ((gettype($grp->Cols) == "array") &&
                    (in_array($grp->Filter->Seed[$i], $target_grp->Cols))) {
                    $error = 1;
                };
            } while ((!$error) && ($i-- >= 0));
        }; // while

        $this->Error = 0;
        return $result;
    } // getFilterSeedGroup


    /** Returns all seed group ids in purkki
     *  @return array/any Array of filter seed group ids.
     */
    function getFilterSeedGroups() {
        $result = array();
        $filtered = $this->getFilteredGroups();
        foreach ($filtered as $grp_id) {
            $seed_grp_id = $this->getFilterSeedGroup($grp_id);
            if ($seed_grp_id != -1) $result[] = $seed_grp_id;
        };

        $this->Error = 0;
        return $result;
    } // getfilterSeedGroups


    /** Renames a column in certain store.
     *  @param string $StoreID Store ID
     *  @param string $OldName Column name to rename
     *  @param string $NewName New column name
     *  @return bool Returns true(1) unless in case of a bad store ID or
     *  column name when false(0) is returned.
     */
    function renameCol($StoreID, $OldName, $NewName) {
        if (!$this->isStore($StoreID)) return 0; // <-- EXIT
        return $this->__Stores[$StoreID]->renameItem($OldName, $NewName);
    } // renameCol


    /** Create the where val list for the fetch request
     *  This list can be sent to Laituri as a list of where keys when sending
     *  fetch request.
     *  @param array/string $WhereData Where data or references to field where
     *                        to take the data
     *  @param string $SrcRow Row of the col that caused the fetch
     *  @return array/str Keys
     */
    function getFetchWhereValues($WhereData, $SrcRow) {
        $result = array();

        foreach($WhereData as $val) {
            /* Check if value was constant or an reference to field on
               the form */
            if ($val[0] == SO_DOLLAR) {
                /* Yep! This was reference. Erase the dollar sign. */
                $col_name = soSubStr($val, 1, strlen($val)-1);

                $group_id = $this->findGroup($col_name);
                $gtype = $this->getGroupType($group_id);
                $row = ($gtype == DZGT_MAIN) ? 0 : $SrcRow;
                /* Get the value of the field */
                $val = $this->getStoreItem(TISKI, $col_name, $row);
             } // if $val
             $result[] = $val;
        } // foreach

        return $result;
    } // getFetchWhereValues


    /** Dump one or more stores as a string.
     * This method is for debuggind purposes.
     * @param string $StoreIDs A comma delimited list of store ids to dump.
     *     An empty string stands for "all stores".
     * @return str Dump string.
     */
    function dump($StoreIDs = "", $RowDelim = ":", $ColDelim = "\n",
                  $StoreDelim = "\n===>") {
        $result = "";
        if (!$StoreIDs) $StoreIDs = DZ_STORE_LIST;
        $store_ids = soExplode($StoreIDs, ",");

        foreach ($store_ids as $store_id) {
            if ($this->isStore($store_id)) {
                $result .= $StoreDelim.$store_id;
                $store = $this->__Stores[$store_id];
                $keys = $store->getkeys();
                foreach ($keys as $key) {
                    $col = $store->getItem($key);
                    $result .= $ColDelim.$key."(".count($col).")=".
                               implode($RowDelim, $col);
                }; // foreach keys
            }; // if isStore
        }; // foreach storeids

        $this->Error = 0;
        return $result;
    } // dump


} // dzPurkki

/* Primitive test code -- remove once filtering features are implemented
$s = new soSet();
$s->addItem("ab", array("aaa", "bbb", "aaa"));
$s->addItem("cd", array("ccc", "ddd", "ccc"));

$u = new soSet();
$u->addItem("ab", array("aaa", "bbb"));

$d = new soSet();
$d->addItem("cd", array("xxx", "yyy"));

$p = new dzPurkki();
$p->setStore(TISKI, $s);

echo $p->updateRows(TISKI, $u, $d)."\n";
#$q = $p->getDistinctRows(TISKI);

#$n = new soSet();
#$n->addItem("ab#-", array(0));
#$p->joinItu(TISKI, $n);

echo $p->dump(TISKI)."\n!\n";
*/

?>
